﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;

namespace Normalify
{
    public interface IBusinessRuleExtractor
    {
        void Load(IDataTable data);
        void Execute(bool resetForEveryFact);
		Action<IDataColumn> OnBeforeReset { get; set; }
    }

	public class BusinessRuleExtractor : IBusinessRuleExtractor
    {
        private IDataTable _table;
        private List<IDataColumn> _facts = new List<IDataColumn>();
        private List<IDataColumn> _dims = new List<IDataColumn>();
        private IDataColumn _currentFact;
        private readonly IDictionary<IDataColumn, BusinessRuleCollection> _busRules = new Dictionary<IDataColumn, BusinessRuleCollection>();

        public void Initialize(IEnumerable<IDataColumn> facts, IEnumerable<IDataColumn> dims)
        {
            _facts = facts.ToList();
            _dims = dims.ToList();
        }

        public void Load(IDataTable data)
        {
            _table = data;
        }

        public BusinessRuleCollection GetRules(IDataColumn fact)
        {
            return _busRules[fact];
        }

		private bool _resetForEveryFact;

        public void Execute(bool resetForEveryFact)
        {
            if (!_facts.Any() || !_dims.Any())
                throw new InvalidOperationException("Please initialize with facts and dims.");
			if (resetForEveryFact && OnBeforeReset == null)
				throw new ArgumentException("Can not set resetForEveryFact if the OnBeforeReset is not handled.");
	        _resetForEveryFact = resetForEveryFact;

            foreach (var fact in _facts)
            {
                _currentFact = fact;
				Console.Write("Analysing {0}", _table.GetName(_currentFact));
				ParseData(new List<IDataColumn>(), _dims.ToList());
				Console.WriteLine();
	            if (_resetForEveryFact)
	            {
		            OnBeforeReset(_currentFact);
		            _busRules.Remove(_currentFact); //to free memory.
					GC.Collect();
	            }
            }
        }

		public Action<IDataColumn> OnBeforeReset { get; set; }

		private const int MinUpdateThr = 200;

        private void ParseData(IList<IDataColumn> prefix, IEnumerable<IDataColumn> columns)
        {
            var colStack = new Stack<IDataColumn>(columns);
            while (colStack.Any())
            {
				Console.Write(".");
                var col = colStack.Pop();
                var updates = BrowseAndUpdate(prefix.Concat(new[] {col}).ToList());
				if (_resetForEveryFact)
				{
					OnBeforeReset(_currentFact);
					_busRules[_currentFact].Rules.Clear();
				}
                if (updates > MinUpdateThr && prefix.Count < 3)
                {
                    ParseData(prefix.Concat(new[] {col}).ToList(), colStack);
                }
            }
        }

        /// <summary>
        /// Go through the table, and update the rules upto prefix. Return the number 
        /// of updates.
        /// </summary>
        private int BrowseAndUpdate(IList<IDataColumn> prefix)
        {
            var rv = 0;
	        var kvps = new Kvp<object>[prefix.Count];
            foreach (var row in _table.Rows)
            {
	            for (var i = (short)0; i < prefix.Count; i++)
	            {
		            var val = row.Get(prefix[i]);
					kvps[i] = new Kvp<object>(prefix[i], val);
	            }
                var factVal =  row.Get(_currentFact);
                rv = Math.Max(rv, UpdateStatistics(kvps, factVal));
            }
            return rv;
        }

        private int UpdateStatistics(IList<Kvp<object>> vals, object factValue)
        {
            if (!_busRules.ContainsKey(_currentFact))
            {
                _busRules.Add(_currentFact, new BusinessRuleCollection());
            }
            var br = _busRules[_currentFact];
            var stat = br.Update(vals, factValue);
            return stat.Count;
        }
    }

    [DebuggerDisplay("{Pair.Item1}={Pair.Item2}")]
    public class Kvp<T> 
    {
        private readonly Tuple<IDataColumn, T> _tuple;

        public Tuple<IDataColumn, T> Pair {get { return _tuple; }}

        public Kvp(IDataColumn key, T value)
        {
            _tuple = Tuple.Create(key, value);
        }

        public override bool Equals(object obj)
        {
            return obj != null && ((Kvp<T>)obj)._tuple.Equals(_tuple);
        }

        public override int GetHashCode()
        {
            return _tuple.GetHashCode();
        }
    }

    public class MultiKey<T>
    {
        private readonly List<Kvp<T>> _items = new List<Kvp<T>>();
        private readonly int _hashCode;

        public IEnumerable<Kvp<T>> Items
        {
            get { return _items; }
        }

        public MultiKey(IList<Kvp<T>> keys)
        {
			_items = new List<Kvp<T>>(keys.Count);
			_items.AddRange(keys);
			//Note: below like is not generic ans is only used for performance reasons.
            _hashCode = _items.Where(kvp => kvp.Pair.Item2 != null).Aggregate(0, (agg, o) => agg + o.GetHashCode()*32);
        }

        public override bool Equals(object obj)
        {
            var other = (MultiKey<T>)obj;
            if (other._items.Count != _items.Count)
                return false;
            if (_items.Count == 0)
                return false;
            return !_items.Where((t, i) => t.Pair.Item2 != null && !t.Equals(other._items[i])).Any();
        }

        public override int GetHashCode()
        {
            return _hashCode;
        }
    }
}
